#!/usr/bin/env node

/**
 * XappTool 前端代码混淆工具
 * 用于保护源代码，防止源码泄露
 */

const { Command } = require('commander');
const JavaScriptObfuscator = require('javascript-obfuscator');
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const ora = require('ora');
const archiver = require('archiver');
const { glob } = require('glob');

const program = new Command();

// 默认配置
const DEFAULT_CONFIG = {
  compact: true,
  controlFlowFlattening: true,
  controlFlowFlatteningThreshold: 0.75,
  deadCodeInjection: true,
  deadCodeInjectionThreshold: 0.4,
  debugProtection: true,
  debugProtectionInterval: 2000,
  disableConsoleOutput: true,
  identifierNamesGenerator: 'hexadecimal',
  log: false,
  numbersToExpressions: true,
  renameGlobals: true,
  rotateStringArray: true,
  selfDefending: true,
  shuffleStringArray: true,
  simplify: true,
  splitStrings: true,
  splitStringsChunkLength: 10,
  stringArray: true,
  stringArrayEncoding: ['base64'],
  stringArrayIndexShift: true,
  stringArrayWrappersCount: 2,
  stringArrayWrappersChainedCalls: true,
  stringArrayWrappersParametersMaxCount: 4,
  stringArrayWrappersType: 'variable',
  stringArrayThreshold: 0.75,
  transformObjectKeys: true,
  unicodeEscapeSequence: true
};

// 混淆配置
const OBFUSCATION_LEVELS = {
  light: {
    ...DEFAULT_CONFIG,
    controlFlowFlattening: false,
    deadCodeInjection: false,
    debugProtection: false,
    selfDefending: false,
    stringArrayEncoding: ['none']
  },
  medium: {
    ...DEFAULT_CONFIG,
    controlFlowFlatteningThreshold: 0.5,
    deadCodeInjectionThreshold: 0.2
  },
  heavy: {
    ...DEFAULT_CONFIG,
    controlFlowFlatteningThreshold: 0.9,
    deadCodeInjectionThreshold: 0.6,
    stringArrayWrappersCount: 5,
    stringArrayThreshold: 0.9
  }
};

// 目标项目配置
const TARGETS = {
  merchant: {
    name: '商户前台',
    source: '../merchant/src',
    output: '../merchant/dist-obfuscated',
    include: ['**/*.js', '**/*.vue'],
    exclude: ['node_modules/**', 'dist/**', 'tests/**'],
    level: 'medium'
  },
  landing: {
    name: '落地页',
    source: '../merchant/src/views/site',
    output: '../merchant/dist-landing-obfuscated',
    include: ['**/*.js', '**/*.vue'],
    exclude: ['node_modules/**'],
    level: 'medium'
  },
  sdk: {
    name: 'SDK',
    source: '../sdksource',
    output: '../sdk-obfuscated',
    include: ['**/*.js'],
    exclude: ['node_modules/**', 'dist/**'],
    level: 'heavy'
  },
  all: {
    name: '全部项目',
    targets: ['merchant', 'landing', 'sdk']
  }
};

/**
 * 混淆单个文件
 */
async function obfuscateFile(filePath, outputPath, options) {
  try {
    const code = await fs.readFile(filePath, 'utf8');
    const obfuscationResult = JavaScriptObfuscator.obfuscate(code, options);
    const obfuscatedCode = obfuscationResult.getObfuscatedCode();
    
    await fs.ensureDir(path.dirname(outputPath));
    await fs.writeFile(outputPath, obfuscatedCode);
    
    return {
      success: true,
      originalSize: code.length,
      obfuscatedSize: obfuscatedCode.length,
      compressionRatio: ((code.length - obfuscatedCode.length) / code.length * 100).toFixed(2)
    };
  } catch (error) {
    return {
      success: false,
      error: error.message
    };
  }
}

/**
 * 混淆项目
 */
async function obfuscateProject(targetName, options = {}) {
  const target = TARGETS[targetName];
  if (!target) {
    throw new Error(`未知目标: ${targetName}`);
  }

  const spinner = ora(`正在混淆 ${target.name}...`).start();
  
  try {
    const sourceDir = path.resolve(__dirname, target.source);
    const outputDir = path.resolve(__dirname, target.output);
    
    // 清理输出目录
    await fs.remove(outputDir);
    
    // 获取文件列表
    const patterns = target.include.map(pattern => path.join(sourceDir, pattern));
    const files = [];
    
    for (const pattern of patterns) {
      const matches = await glob(pattern, {
        ignore: target.exclude.map(ex => path.join(sourceDir, ex))
      });
      files.push(...matches);
    }
    
    spinner.text = `找到 ${files.length} 个文件`;
    
    let processed = 0;
    let totalOriginalSize = 0;
    let totalObfuscatedSize = 0;
    const errors = [];
    
    // 混淆配置
    const obfuscationOptions = OBFUSCATION_LEVELS[options.level || target.level];
    
    // 处理每个文件
    for (const file of files) {
      const relativePath = path.relative(sourceDir, file);
      const outputPath = path.join(outputDir, relativePath);
      
      spinner.text = `处理中... (${++processed}/${files.length}) ${relativePath}`;
      
      // 复制非JS文件
      if (!file.endsWith('.js') && !file.endsWith('.vue')) {
        await fs.copy(file, outputPath);
        continue;
      }
      
      // 混淆JS/Vue文件
      const result = await obfuscateFile(file, outputPath, {
        ...obfuscationOptions,
        sourceMapFileName: options.sourceMap ? `${path.basename(file)}.map` : undefined
      });
      
      if (result.success) {
        totalOriginalSize += result.originalSize;
        totalObfuscatedSize += result.obfuscatedSize;
      } else {
        errors.push({ file: relativePath, error: result.error });
      }
    }
    
    // 复制其他文件（配置文件、图片等）
    const otherFiles = await glob(path.join(sourceDir, '**/*'), {
      ignore: [...target.exclude.map(ex => path.join(sourceDir, ex)), ...patterns]
    });
    
    for (const file of otherFiles) {
      const relativePath = path.relative(sourceDir, file);
      const outputPath = path.join(outputDir, relativePath);
      await fs.copy(file, outputPath);
    }
    
    spinner.succeed(`${target.name} 混淆完成`);
    
    // 输出统计信息
    console.log(chalk.green(`\n混淆统计:`));
    console.log(chalk.blue(`  处理文件: ${files.length}`));
    console.log(chalk.blue(`  原始大小: ${(totalOriginalSize / 1024).toFixed(2)} KB`));
    console.log(chalk.blue(`  混淆大小: ${(totalObfuscatedSize / 1024).toFixed(2)} KB`));
    console.log(chalk.blue(`  压缩率: ${((totalOriginalSize - totalObfuscatedSize) / totalOriginalSize * 100).toFixed(2)}%`));
    
    if (errors.length > 0) {
      console.log(chalk.yellow(`\n警告: ${errors.length} 个文件处理失败`));
      errors.forEach(({ file, error }) => {
        console.log(chalk.red(`  ${file}: ${error}`));
      });
    }
    
    // 创建压缩包
    if (options.zip) {
      await createZipArchive(outputDir, `${targetName}-obfuscated.zip`);
    }
    
    return {
      success: true,
      filesProcessed: files.length,
      totalOriginalSize,
      totalObfuscatedSize,
      errors: errors.length
    };
    
  } catch (error) {
    spinner.fail(`混淆失败: ${error.message}`);
    throw error;
  }
}

/**
 * 创建压缩包
 */
async function createZipArchive(sourceDir, outputFile) {
  const spinner = ora('创建压缩包...').start();
  
  return new Promise((resolve, reject) => {
    const output = fs.createWriteStream(path.join(__dirname, outputFile));
    const archive = archiver('zip', { zlib: { level: 9 } });
    
    output.on('close', () => {
      spinner.succeed(`压缩包创建完成: ${outputFile} (${(archive.pointer() / 1024 / 1024).toFixed(2)} MB)`);
      resolve();
    });
    
    archive.on('error', (err) => {
      spinner.fail('压缩包创建失败');
      reject(err);
    });
    
    archive.pipe(output);
    archive.directory(sourceDir, false);
    archive.finalize();
  });
}

/**
 * 主函数
 */
async function main() {
  program
    .name('xapptool-obfuscator')
    .description('XappTool 前端代码混淆工具')
    .version('1.0.0');
  
  program
    .command('obfuscate <target>')
    .description('混淆指定项目')
    .option('-l, --level <level>', '混淆级别 (light, medium, heavy)', 'medium')
    .option('-z, --zip', '创建压缩包')
    .option('-s, --source-map', '生成source map')
    .option('-o, --output <dir>', '输出目录')
    .action(async (target, options) => {
      try {
        console.log(chalk.blue('=== XappTool 代码混淆工具 ===\n'));
        
        if (target === 'all') {
          // 处理所有目标
          const results = [];
          for (const subTarget of TARGETS.all.targets) {
            const result = await obfuscateProject(subTarget, options);
            results.push({ target: subTarget, ...result });
          }
          
          console.log(chalk.green('\n=== 全部项目混淆完成 ==='));
          results.forEach(({ target, filesProcessed, totalOriginalSize, totalObfuscatedSize }) => {
            console.log(chalk.blue(`${target}: ${filesProcessed} 个文件, 压缩率: ${((totalOriginalSize - totalObfuscatedSize) / totalOriginalSize * 100).toFixed(2)}%`));
          });
        } else {
          // 处理单个目标
          await obfuscateProject(target, options);
        }
        
        console.log(chalk.green('\n✅ 混淆完成！'));
        
      } catch (error) {
        console.error(chalk.red(`\n❌ 错误: ${error.message}`));
        process.exit(1);
      }
    });
  
  program
    .command('list')
    .description('列出可用的混淆目标')
    .action(() => {
      console.log(chalk.blue('可用的混淆目标:'));
      Object.entries(TARGETS).forEach(([key, target]) => {
        if (key !== 'all') {
          console.log(chalk.green(`  ${key.padEnd(12)} - ${target.name} (${target.level})`));
        }
      });
      console.log(chalk.green(`  ${'all'.padEnd(12)} - ${TARGETS.all.name}`));
    });
  
  program
    .command('config')
    .description('显示混淆配置')
    .option('-l, --level <level>', '配置级别 (light, medium, heavy)', 'medium')
    .action((options) => {
      const config = OBFUSCATION_LEVELS[options.level];
      console.log(chalk.blue(`=== ${options.level.toUpperCase()} 级别混淆配置 ===\n`));
      console.log(JSON.stringify(config, null, 2));
    });
  
  await program.parseAsync(process.argv);
}

// 运行主函数
if (require.main === module) {
  main().catch(error => {
    console.error(chalk.red(`致命错误: ${error.message}`));
    process.exit(1);
  });
}

module.exports = {
  obfuscateProject,
  OBFUSCATION_LEVELS,
  TARGETS
};